﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tesla.Graphics;
using Tesla.Math;
using Tesla.Content;
using Tesla.Scene.Shape;
using Tesla.Scene;
using Tesla.Input;
using Tesla.Core;

namespace TeslaSamples.Effects {
    [AppDescription("Shadow Mapping", PlatformCapabilities.Direct3D10 | PlatformCapabilities.XNA4, "ShadowImage", "ShadowDesc", "ShadowSource")]
    public class ShadowMappingSample : BasicApp {
        private RenderTarget2D shadowMap;
        private RenderTarget2D blurRT;
        private Effect blurEffect;
        private Material shadowMat;
        private Mesh mesh;
        private SpotLight sl;
        private Camera lightCam;
        private DepthStencilState dss;
        private SpriteBatch batch;
        private Matrix texMat;
        private Material receiverMat;
        private bool receive = false;
        private RasterizerState rs;
        private Node rtnode;
        private float blurAmount = 2f;
        private bool cullFront = false;

        protected override void LoadContent() {
            ClearColor = Color.Black;
            shadowMap = new RenderTarget2D(1024, 1024, false, SurfaceFormat.Single, DepthFormat.Depth24Stencil8);
            blurRT = new RenderTarget2D(1024, 1024, false, SurfaceFormat.Single, DepthFormat.None);
            blurEffect = ContentManager.Load<Effect>("Shaders//GaussianBlur.fx");

            ModelLoaderParameters mlp = new ModelLoaderParameters();
            mlp.NormalGeneration = NormalGeneration.Smooth;
            mlp.SwapWindingOrder = true;
            mlp.PreferLitMaterials = true;

            rs = new RasterizerState();
            rs.Cull = CullMode.Front;
            rs.BindRenderState();

           // mesh = new Sphere("Sphere", 50, 50, 20);
            mesh = ContentManager.Load<Mesh>("Models//statue.tebo");
            mesh.ComputeTangentBasis();
            Material mat = ContentManager.Load<Material>("LitBasicTexture.tem").Clone();
            mat.SetParameter("DiffuseMap", ContentManager.Load<Texture2D>("Textures//statue_diff.dds"));
            mat.SetParameter("NormalMap", ContentManager.Load<Texture2D>("Textures//statue_norm.dds"));
            mat.SetParameter("MatSpecular", new Vector3(.3f, .3f, .3f));
            mesh.SetMaterial(mat);

            Quad q = new Quad("Floor", 800, 800);
            q.Rotation = Quaternion.FromAngleAxis(MathHelper.ToRadians(-90), Vector3.UnitX);
            q.Translation = new Vector3(0, -70, 0);
            q.Material = ContentManager.Load<Material>("Materials//LitBasicTextureShadow.tem").Clone();
            q.Material.SetParameter("DiffuseMap", ContentManager.Load<Texture2D>("Textures//statue_diff.dds"));
            receiverMat = q.Material;
            receiverMat.SetParameter("MatSpecular", new Vector3(.3f, .3f, .3f));
           // mesh.SetMaterial(receiverMat);

            InputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.Enter, false), new InputAction(delegate(GameTime time) {
                receive = !receive;
                if(receive) {
                    mesh.SetMaterial(receiverMat);
                } else {
                    mesh.SetMaterial(mat);
                }
            })));
            InputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.C, false), new InputAction(delegate(GameTime time) {
                cullFront = !cullFront;
            })));

            DataBuffer<Vector2> texCoords = q.MeshData.TextureCoordinates;
            texCoords.Position = 0;
            texCoords.Set(new Vector2(0, 0));
            texCoords.Set(new Vector2(4, 0));
            texCoords.Set(new Vector2(4, 4));
            texCoords.Set(new Vector2(0, 4));
            q.MeshData.UpdateVertexData<Vector2>(VertexSemantic.TextureCoordinate, texCoords);
            RootNode.AddChild(q);

            sl = new SpotLight(new Vector3(100, 200, 10), new Vector3(-100, -200, -10), 5, 15);
            RootNode.RemoveAllLights();
            RootNode.AddLight(sl);
            RootNode.AddChild(mesh);


            shadowMat = new Material("ShadowMat");
            shadowMat.LoadEffect(ContentManager.Load<Effect>("Shaders//ShadowMapEffect.fx"));
            shadowMat.SetEngineParameter("LightWVP", Tesla.Core.EngineValue.WorldViewProjection);
            lightCam = new Camera();

            batch = new SpriteBatch();
            dss = new DepthStencilState();
            dss.DepthFunction = ComparisonFunction.LessEqual;
            dss.BindRenderState();

            float texOffsets = 0.5f + (0.5f / (float) shadowMap.Width);
            texMat = new Matrix(0.5f, 0.0f, 0.0f, 0.0f, 
                                0.0f, -0.5f, 0.0f, 0.0f,
                                0.0f, 0.0f, 1.0f, 0.0f,
                                texOffsets, texOffsets, 0.0f, 1.0f);
            rtnode = new Node("Node");
            rtnode.Translation = sl.Position;
            Node parent = new Node("Parent");
            parent.AddChild(rtnode);
            RootNode.AddChild(parent);
            parent.AddController(new Tesla.Scene.Extension.RotateController(Vector3.UnitY, 25));
        }

        protected override void OnUpdate(GameTime time) {
            base.OnUpdate(time);
            sl.Position = rtnode.WorldTranslation;
            sl.Direction = -sl.Position;
            RootNode.UpdateWorldLights(true);
        }

        protected override void OnRenderPre(IRenderer renderer) {
            base.OnRenderPre(renderer);

            ICamera oldCam = renderer.CurrentCamera;
            lightCam.Viewport = new Viewport(0, 0, shadowMap.Width, shadowMap.Height);// oldCam.Viewport;
            lightCam.SetFrame(sl.Position, Matrix.LookAt(sl.Position, Vector3.Zero, Vector3.Up));
            lightCam.SetProjection(ProjectionMode.Orthographic, 400, 400, 1.0f, 1000.0f);
           // lightCam.SetProjection(45f, 1f, 500f);
            lightCam.Update();

            Viewport viewport = lightCam.Viewport;
            float invWidth = (viewport.Width > 0) ? (1f / ((float) viewport.Width)) : 0f;
            float invHeight = (viewport.Height > 0) ? (-1f / ((float) viewport.Height)) : 0f;
            Matrix matrix = new Matrix();
            matrix.M11 = invWidth * 2f;
            matrix.M22 = invHeight * 2f;
            matrix.M33 = 1f;
            matrix.M44 = 1f;
            matrix.M41 = -1f;
            matrix.M42 = 1f;
            matrix.M41 = matrix.M41 - invWidth;
            matrix.M42 = matrix.M42 - invHeight;

            Matrix projection = Matrix.CreateOrthoMatrix(0, viewport.Width, viewport.Height, 0, 0, 1);
            blurEffect.Parameters["SpriteTransform"].SetValue(projection);

            Matrix ortho = Matrix.CreateOrthoMatrix(30, 30, 1.0f, 200f);
            Matrix view = Matrix.CreateViewMatrix(sl.Position, Vector3.Zero, Vector3.Up);//Matrix.LookAt(sl.Position, Vector3.Zero, Vector3.Up);
            Matrix viewproj = view * ortho;
            Matrix viewprojcam = lightCam.ViewProjectionMatrix;

            renderer.CurrentCamera = lightCam;

            renderer.SetRenderTarget(shadowMap);
            renderer.Clear(Color.White);

            renderer.EnforcedMaterial = shadowMat;
            renderer.EnforcedDepthStencilState = dss;
            if(cullFront) {
                renderer.EnforcedRasterizerState = rs;
            }
            mesh.Render(renderer);
            renderer.ClearEnforcedStates();
            renderer.EnforcedMaterial = null;
            /*
            //Pass 2 - Draw from shadow to blur rt blur horizontally
            renderer.EnforcedRasterizerState = RasterizerState.CullNone;
            renderer.SetRenderTarget(blurRT);
            SetBlurEffectParameters(1.0f / (float) blurRT.Width, 0);
            DrawFullScreenQuad(shadowMap, BlendState.Opaque, blurRT.Width, blurRT.Height, blurEffect);

            //Pass 3 - Draw from blurRT to shadowmap, blur vertically
            renderer.SetRenderTarget(shadowMap);
            SetBlurEffectParameters(0, 1.0f / (float) shadowMap.Height);
            DrawFullScreenQuad(blurRT, BlendState.Opaque, shadowMap.Width, shadowMap.Height, blurEffect);
            */
            renderer.CurrentCamera = oldCam;
            renderer.SetRenderTarget(null);

            receiverMat.SetParameter("LightVP", lightCam.ViewProjectionMatrix);
            receiverMat.SetParameter("LightDirection", sl.Direction);
            receiverMat.SetParameter("ShadowMap", shadowMap);
        }

        protected override void OnRenderPost(IRenderer renderer) {
            base.OnRenderPost(renderer);

            batch.Begin(SpriteSortMode.Immediate, BlendState.Opaque);
            batch.Draw(shadowMap, new Rectangle(0, 0, renderer.Viewport.Width/4, renderer.Viewport.Height/4), Color.White);
            batch.End();
        }

        private void DrawFullScreenQuad(Texture2D texture, BlendState bs, int width, int height, Effect customEffect) {
            batch.Begin(SpriteSortMode.Immediate, bs, null, null, null, customEffect);
            batch.Draw(texture, new Rectangle(0, 0, width, height), Color.White);
            batch.End();
        }

        /// <summary>
        /// Computes sample weightings and texture coordinate offsets
        /// for one pass of a separable gaussian blur filter.
        /// </summary>
        private void SetBlurEffectParameters(float dx, float dy) {
            // Look up the sample weight and offset effect parameters.
            IEffectParameter weightsParameter, offsetsParameter;

            weightsParameter = blurEffect.Parameters["SampleWeights"];
            offsetsParameter = blurEffect.Parameters["SampleOffsets"];

            // Look up how many samples our gaussian blur effect supports.
            int sampleCount = weightsParameter.Elements.Count;

            // Create temporary arrays for computing our filter settings.
            float[] sampleWeights = new float[sampleCount];
            Vector2[] sampleOffsets = new Vector2[sampleCount];

            // The first sample always has a zero offset.
            sampleWeights[0] = ComputeGaussian(0);
            sampleOffsets[0] = new Vector2(0);

            // Maintain a sum of all the weighting values.
            float totalWeights = sampleWeights[0];

            // Add pairs of additional sample taps, positioned
            // along a line in both directions from the center.
            for(int i = 0; i < sampleCount / 2; i++) {
                // Store weights for the positive and negative taps.
                float weight = ComputeGaussian(i + 1);

                sampleWeights[i * 2 + 1] = weight;
                sampleWeights[i * 2 + 2] = weight;

                totalWeights += weight * 2;

                // To get the maximum amount of blurring from a limited number of
                // pixel shader samples, we take advantage of the bilinear filtering
                // hardware inside the texture fetch unit. If we position our texture
                // coordinates exactly halfway between two texels, the filtering unit
                // will average them for us, giving two samples for the price of one.
                // This allows us to step in units of two texels per sample, rather
                // than just one at a time. The 1.5 offset kicks things off by
                // positioning us nicely in between two texels.
                float sampleOffset = i * 2 + 1.5f;

                Vector2 delta = new Vector2(dx, dy) * sampleOffset;

                // Store texture coordinate offsets for the positive and negative taps.
                sampleOffsets[i * 2 + 1] = delta;
                sampleOffsets[i * 2 + 2] = -delta;
            }

            // Normalize the list of sample weightings, so they will always sum to one.
            for(int i = 0; i < sampleWeights.Length; i++) {
                sampleWeights[i] /= totalWeights;
            }

            // Tell the effect about our new filter settings.
            weightsParameter.SetValue(sampleWeights);
            offsetsParameter.SetValue(sampleOffsets);
        }


        /// <summary>
        /// Evaluates a single point on the gaussian falloff curve.
        /// Used for setting up the blur filter weightings.
        /// </summary>
        private float ComputeGaussian(float n) {
            float theta = blurAmount;

            return (float) ((1.0 / Math.Sqrt(2 * Math.PI * theta)) *
                           Math.Exp(-(n * n) / (2 * theta * theta)));
        }
    }
}
